package dungeonPackage;

import java.util.*;

import squareBorderPackage.*;

/**
 * A class of Dungeons containing squares.
 * 
 * @invar
 * 
 * @version 1.0
 * @author Jessa Bekker & Esther Alberts
 */
public class Dungeon {
	/**
	 * Initialize this Dungeon with the given orientation as maximum
	 * coordinates.
	 * 
	 * @param 	maxCoordinates
	 *          The Orientation of the maximum coordinates for this new dungeon.
	 * @post 	The given orientation is the orientation of the maximum coordinates
	 *       	of this new square. 
	 *       	| new.getMaxCoordinates() == maxCoordinates
	 */
	public Dungeon(Orientation maxCoordinates) {
		this.setMaxCoordinates(maxCoordinates);

	}

	/**
	 * Return the orientation of the maximum coordinates.
	 */
	public Orientation getMaxCoordinates() {
		return maxCoordinates;
	}

	/**
	 * Checks whether this Dungeon can have the given coordinates as maximum
	 * coordinates.
	 * 
	 * @param 	coordinates
	 *          The coordinates to check.
	 * @return 	False if the given orientation is null 
	 * 			| if (coordinates == null)
	 *         	| then (result == false)
	 * @return 	False if one or more of the coordinates of the current maximum
	 *         	coordinates of this Dungeon are larger than the corresponding
	 *         	given coordinates . 
	 *         	| if (this.getMaxCoordinates().getX() > coordinates.getX() 
	 *         	| || this.getMaxCoordinates().getY() > coordinates.getY() 
	 *         	| || this.getMaxCoordinates().getZ() > coordinates.getZ()) 
	 *         	| then (result == false)
	 */
	public boolean canHaveAsMaxCoordinates(Orientation coordinates) {
		if (coordinates == null)
			return false;
		if (this.getMaxCoordinates().getX() >= coordinates.getX()
				|| this.getMaxCoordinates().getY() >= coordinates.getY()
				|| this.getMaxCoordinates().getZ() >= coordinates.getZ())
			return false;
		return true;
	}

	/**
	 * Set the orientation of the maximum coordinates to the given orientation.
	 * 
	 * @param 	maxCoordinates
	 *          The orientation to be registered as maximum coordination.
	 * @post 	The new orientation of the maximum coordinations is equal to the given orientation. 
	 * 			| new.getMaxCoordinates() == coordinates
	 * @throws 	IllegalArgumentException
	 *          This Dungeon can't have the given Orientation as orientation
	 *          of its maximum coordinates. 
	 *          | !CanHaveAsMaxCoordinates(maxCoordinates)
	 */
	public void setMaxCoordinates(Orientation maxCoordinates)
			throws IllegalArgumentException {
		if (!canHaveAsMaxCoordinates(maxCoordinates))
			throw new IllegalArgumentException("Non effective orientation");
		this.maxCoordinates = maxCoordinates;
	}

	/**
	 * Variable registering the orientation of the maximum coordinates.
	 */
	private Orientation maxCoordinates = new Orientation(0, 0, 0);


	/**
	 * Return the square at the given position
	 * 
	 * @param 	orientation
	 *          The position of the wanted square
	 * @throws 	InvallidArgumentException
	 *          The given orientation is not effective. 
	 *          | orientation == null
	 */
	public Square getSquareAt(Orientation orientation)
			throws IllegalArgumentException {
		if (orientation == null)
			throw new IllegalArgumentException("non effective orientation");
		return squares.get(orientation);
	}

	/**
	 * Return a map with all the squares and its orientations as keys.
	 */
	public Map<Orientation, Square> getSquares() {
		return new HashMap<Orientation, Square>(squares);
	}

	/**
	 * Check whether the given orientation is a valid orientation for a square
	 * in a dungeon.
	 * 
	 * @param 	orientation
	 *          The orientation to check.
	 * @return 	False if the orientation is non-effective 
	 * 			| if (orientation == null) 
	 * 			| then (result == false)
	 * @return 	False if the x-coordinate, the y-coordinate and the z-coordinate
	 *         	have the same values. 
	 *         	| if (orientation.getX() == orientation.getY() && orientation.getX() == orientation.getZ())
	 *         	| then (result == false)
	 * @return 	False if the given orientation is outside the orientation of the maximum coordinates. 
	 * 			| if (orientation.isOutside(getMaxCoordinates())) 
	 * 			| then (result == false)
	 */
	public boolean isValidOrientation(Orientation orientation) {
		if (orientation == null)
			return false;
		if (orientation.getX() == orientation.getY()
				&& orientation.getX() == orientation.getZ())
			return false;
		if (orientation.isOutside(getMaxCoordinates()))
			return false;
		return true;
	}

	/**
	 * Check whether the given square is a valid square.
	 * 
	 * @param 	square
	 *          The square to check.
	 * @return 	False if the given square is not effective. 
	 * 			| if(square == null)
	 *         	| then (result == false)
	 * @return 	False if the given square has no valid humidity. 
	 * 			| if( !square.isValidHumidity(square.getHumidity())) 
	 * 			| then (result == false)
	 * @return	False if for some direction this square has no valid border.
	 * 			| if (for some direction in Direction.getListOfDirections():
	 * 			|		square.getBorderAt(direction) == null)
	 * 			|	then (result == false) 
	 */
	public boolean isValidSquare(Square square) {
		if (square == null)
			return false;
		if (!square.isValidHumidity(square.getHumidity()))
			return false;
		for(Direction direction: Direction.getListOfDirections())
			if(square.getBorderAt(direction) == null)
				return false;
		return true;
	}

	/**
	 * Check whether this dungeon can have the given square at the given orientation.
	 * 
	 * @param 	square
	 * 			The square to check.
	 * @param 	orientation
	 * 			The orientation to check.
	 * @return 	False if the given square is not valid. 
	 * 			| if (!isValidSquare(square)) 
	 * 			| then ( result == false)
	 * @return 	False if the given orientation is not valid. 
	 * 			| if(!isValidOrientation(orientation)) 
	 * 			| then ( result == false)
	 * @return 	False if this dungeon already contains the given square and the
	 *         	square at the given orientation is not the given square. 
	 *         	| if (squares.containsValue(square) && getSquares().get(orientation) != square) 
	 *         	| then (result == false)
	 */
	public boolean canHaveAsSquareAt(Square square, Orientation orientation) {
		if (!isValidSquare(square))
			return false;
		if (!isValidOrientation(orientation))
			return false;
		if (squares.containsValue(square)
				&& getSquares().get(orientation) != square)
			return false;
		return true;
	}

	/**
	 * Check whether there is a square at the given orientation.
	 * 
	 * @param 	orientation
	 *          The orientation to check.
	 * @return 	True if and only if there is a square at the given orientation. 
	 * 			| if (getSquareAt(orientation) != null) 
	 * 			|	then (result == true) 
	 * 			| else 
	 * 			|	(result == false)
	 * @throws 	IllegalArgumentException
	 *          The given orientation is a not effective orientation.
	 *          | orientation == null
	 */
	public boolean hasSquareAt(Orientation orientation)
			throws IllegalArgumentException {
		if (orientation == null)
			throw new IllegalArgumentException("non effective orientation");
		return (getSquareAt(orientation) != null);
	}

	/**
	 * Remove the square at the given orientation
	 * 
	 * @param 	orientation
	 *          The orientation to remove the square from.
	 * @post	The square at the given orientation will be terminated.
	 * 			| this.getSquares().get(orientation).isTerminated
	 * @post 	There is no square at the given orientation of the new squares map.
	 *       	| new.squares.hasSquareAt(orientation) == false
	 * @throws 	IllegalArgumentException
	 * 			The given orientation is a non effective orientation.
	 * 			| orientation != null
	 */
	public void removeSquareAt(Orientation orientation)
			throws IllegalArgumentException {
		if (orientation == null)
			throw new IllegalArgumentException("non effective orientation");
		getSquares().get(orientation).terminate();
		squares.remove(orientation);
	}

	/**
	 * Register a reference to the given square at the given orientation.
	 * 
	 * @param 	orientation
	 *          The orientation where to register the reference to the given square.
	 * @param 	square
	 *         	The square to register a reference to.
	 * @post	If there is a square at the given orientation, that square will be terminated.
	 * 			| this.getSquares().get(orientation).isTerminated()
	 * @post 	The square at the given orientation in this dungeon, is the given square. 
	 *       	| new.getSquareAt(orientation) == square
	 * @post	The borders between the given square and its neighbors will be the strongest
	 * 			border of the two borders they have in between.
	 * 			| for each direction of Direction.getListOfDirections() :
	 * 			| 	new.square.getBorderAt(direction) == (new neighbor).getBorderAt(direction.getOpositeDirection())
	 * 			|	&& new.square.getBorderAt(direction) == this.square.getBorderAt(direction).getStrongestBorder(
	 * 			|				(this neighbor).getBorderAt(direction.getOpositeDirection()))
	 * @effect	If a shared border of this square and a neighbor is no obstacle, their spaces
	 * 			will be merged. 
	 * 			| for each direction of Direction.getListOfDirections() :
	 * 			| 	if( new.square.getBorderAt(direction).isObstacle)
	 * 			|		then (this.square.mergeWith(neighbor)
	 * @post 	All the squares in the space of the new square will have the same
	 *       	temperature and humidity degree.
	 *       	| (! (for some value in getSpace(orientation).values(): 
	 *       	| (! value.getTemperature() != new.square.getTemperature) || 
	 *       	| (! value.getHumidity() != new.square.getHumidity))
	 * @throws 	IllegalArgumentException
	 *          This dungeon can not have the given square at the given orientation. 
	 *          | (! canHaveAsSquareAt(square, orientation))
	 * @throws 	InvalidSquareAdditionException
	 *          The percentage of squares with a slippery flooring would get
	 *			larger than the maximum percentage of squares with a slippery
	 *          flooring in a dungeon.
	 *          | square.getSlipperyFlooring() && !getSquares().get(orientation).getSlipperyFlooring()
				|	&& !canAddSlipperyFlooring()
	 */
	public void setSquareAt(Orientation orientation, Square square)
			throws IllegalArgumentException, InvalidSquareAdditionException {
		if (!canHaveAsSquareAt(square, orientation))
			throw new IllegalArgumentException(
					"This dungeon can not have the given square at the given orientation.");
		if (square.getSlipperyFlooring() && !getSquares().get(orientation).getSlipperyFlooring()
					&& !canAddSlipperyFlooring())
			throw new InvalidSquareAdditionException(this, square);
		if(getSquares().get(orientation) != null)
			getSquares().get(orientation).terminate();
		squares.put(orientation, square);
		for (Orientation neighbor : getNeighbors(orientation)) {
			Border border = setSharedBorder(orientation, neighbor);
			if (!border.isObstacle())
				square.mergeWith(getSquareAt(neighbor));
		}
	}

	/**
	 * Map collecting references to the squares of this dungeon with its
	 * orientations as keys.
	 */
	private Map<Orientation, Square> squares = new HashMap<Orientation, Square>();

	/**
	 * Return the number of squares with a slippery flooring in this dungeon.
	 * 
	 * @return 	The number of squares with a slippery flooring in this dungeon.
	 */
	public int getNumberSlippery() {
		int numberSlippery = 0;
		for (Square square : squares.values())
			if (square.getSlipperyFlooring())
				numberSlippery++;
		return numberSlippery;
	}

	/**
	 * Return the percentage of squares with a slippery flooring in this
	 * dungeon.
	 * 
	 * @return 	The number of squares with a slippery flooring divided by the
	 *         	number of squares in this dungeon multiplied with 100. 
	 *         	| result == 100*getNumberSlippery()/squares.size()
	 */
	public double getSlipperyPercentage() {
		return 100 * getNumberSlippery() / squares.size();
	}

	/**
	 * Return whether a square with a slippery flooring can be added to this
	 * dungeon.
	 * 
	 * @return True if and only if the new percentage of squares with a slippery
	 *         flooring is smaller or equal to the maximum percentage of squares
	 *         with a slippery flooring. 
	 *         | result == (100*(getNumberSlippery() + 1)/(squares.size() + 1) 
	 *         | <= MAX_SLIPPERY_PERCENTAGE)
	 */
	public boolean canAddSlipperyFlooring() {
		double newPercentage = 100 * (getNumberSlippery() + 1)
				/ (squares.size() + 1);
		return newPercentage <= MAX_SLIPPERY_PERCENTAGE;
	}

	/**
	 * Variable registering the maximum percentage of squares with a slippery
	 * flooring in a dungeon.
	 */
	private static final double MAX_SLIPPERY_PERCENTAGE = 20;

	/**
	 * Return a set with the orientations of the squares that are neighbors of
	 * the given orientation.
	 * 
	 * @param 	orientation
	 *          The orientation to get the neighbors for.
	 * @return 	A set with the orientations of the squares located next to the
	 *         	given orientation.
	 *         	| for each orientation of orientation.GetTangentOrientation() :
	 *         	|	if(hasSquareAt(orientation))
	 *			|		then (result.contains(orientation))
	 * @throws 	IllegalArgumentException
	 *          The given orientation is not valid. 
	 *          | ! isValidOrientation(orientation)
	 */
	public Set<Orientation> getNeighbors(Orientation orientation)
			throws IllegalArgumentException {
			if (!isValidOrientation(orientation))
				throw new IllegalArgumentException("non valid orientation");

		Set<Orientation> neighbors = new HashSet<Orientation>();

		for (Orientation neighbor : orientation.getTangentOrientations())
			if (hasSquareAt(neighbor))
				neighbors.add(neighbor);

		return neighbors;
	}

	/**
	 * Set the shared border of two directions as the "strongest" border of the two.
	 * 
	 * @param 	orientation1
	 *          The orientation of the first square to set the border from.
	 * @param 	orientation2
	 *          The orientation of the square that borders the first square
	 * @pre		The given orientations have to be effective and tangent.
	 * 			| orientation1 != null && orientation2 != null && orientation1.isTangentTo(orientation2)	
	 * @post 	The squares of orientation1 and orientation2 will have the same
	 *       	type of border, the strongest of the two previous borders, in the
	 *       	direction where they share their border.
	 *       	| new.getSquareAt(orientation1).getBorderAt(orientation1.getSharedBorderDirections(orientation2)[0])
	 *       	|	== new.getSquareAt(orientation2).getBorderAt(orientation2.getSharedBorderDirections(orientation1)[0])
	 *       	|	&& new.getSquareAt(orientation1).getBorderAt(orientation1.getSharedBorderDirections(orientation2)[0])
	 *       	|	== this.getSquareAt(orientation1).getBorderAt(orientation1.getSharedBorderDirections(orientation2)[0]).getStrongestBorder(
	 *       	|		(this.getSquareAt(orientation1).getBorderAt(orientation1.getSharedBorderDirections(orientation2)[0]))
	 * @post	The weakest border will be terminated.
	 * 			| if (this.getSquareAt(orientation1).getBorderAt(orientation1.getSharedBorderDirections(orientation2)[0])
	 * 			|	!= new.getSquareAt(orientation1).getBorderAt(orientation1.getSharedBorderDirections(orientation2)[0]))
	 * 			|	then (this.getSquareAt(orientation1).getBorderAt(orientation1.getSharedBorderDirections(orientation2)[0]).isTerminated())
	 * 			| else(this.getSquareAt(orientation2).getBorderAt(orientation2.getSharedBorderDirections(orientation1)[0]).isTerminated())
	 */
	private Border setSharedBorder(Orientation orientation1, Orientation orientation2) {
		Orientation[] orientations = new Orientation[2];
		orientations[0] = orientation1;
		orientations[1] = orientation2;

		Direction[] borderDirections = orientation1
				.getSharedBorderDirections(orientation2);

		Border newBorder = getSquareAt(orientation1).getBorderAt(
				borderDirections[0]).getStrongestBorder(
				getSquareAt(orientation2).getBorderAt(borderDirections[1]));

		Square[] squares = new Square[2];

		for (int i = 0; i <= 1; i++) {
			squares[i] = getSquareAt(orientations[i]);
			// de zwakke border zoeken
			if (squares[i].getBorderAt(borderDirections[i]) != newBorder
					&& squares[i].getBorderAt(borderDirections[i]) != null) {
					squares[i].getBorders().get(borderDirections[i]).terminate();
			}
		}
		newBorder.attachSquares(squares, borderDirections[0]);
		return newBorder;
	}

}
